'use client' import { useEffect, useState, useRef } from 'react' import { useParams, useRouter } from 'next/navigation' import { useAuth } from '@nextsparkjs/core/hooks/useAuth' import { Button } from '@nextsparkjs/core/components/ui/button' import { Alert, AlertDescription, AlertTitle } from '@nextsparkjs/core/components/ui/alert' import { Loader2, CheckCircle, XCircle, Users, LogIn, UserPlus } from 'lucide-react' import { toast } from 'sonner' import Link from 'next/link' import { sel } from '@nextsparkjs/core/selectors' import { getTemplateOrDefaultClient } from '@nextsparkjs/registries/template-registry.client' type InvitationStatus = 'loading' | 'valid' | 'accepting' | 'accepted' | 'error' | 'expired' | 'not_found' | 'email_mismatch' | 'already_member' | 'requires_auth' interface InvitationInfo { teamName: string inviterName: string role: string email: string } function AcceptInvitePage() { const params = useParams()! const router = useRouter() const { user, isLoading: authLoading } = useAuth() const token = params.token as string const [status, setStatus] = useState('loading') const [invitationInfo, setInvitationInfo] = useState(null) const [errorMessage, setErrorMessage] = useState('') const autoAcceptAttempted = useRef(false) // Validate invitation and auto-accept when user is authenticated useEffect(() => { if (authLoading) return async function validateAndAccept() { try { // First validate the invitation const response = await fetch(`/api/v1/team-invitations/${token}`) const data = await response.json() if (!response.ok) { if (data.code === 'INVITATION_NOT_FOUND') { setStatus('not_found') } else if (data.code === 'INVITATION_EXPIRED') { setStatus('expired') } else { setStatus('error') setErrorMessage(data.error || 'Failed to validate invitation') } return } const info = { teamName: data.data.teamName || 'Unknown Team', inviterName: data.data.inviterName || 'Someone', role: data.data.role, email: data.data.email } setInvitationInfo(info) // Check if user is logged in if (!user) { setStatus('requires_auth') return } // Check email match if ((user.email ?? '').toLowerCase() !== data.data.email.toLowerCase()) { setStatus('email_mismatch') setErrorMessage(`This invitation was sent to ${data.data.email}, but you are logged in as ${user.email}`) return } // Auto-accept the invitation since user is authenticated and email matches if (autoAcceptAttempted.current) return autoAcceptAttempted.current = true setStatus('accepting') const acceptResponse = await fetch(`/api/v1/team-invitations/${token}/accept`, { method: 'POST', credentials: 'include' }) const acceptData = await acceptResponse.json() if (!acceptResponse.ok) { if (acceptData.code === 'ALREADY_MEMBER') { setStatus('already_member') } else if (acceptData.code === 'EMAIL_MISMATCH') { setStatus('email_mismatch') setErrorMessage(acceptData.error) } else if (acceptData.code === 'INVITATION_EXPIRED') { setStatus('expired') } else { setStatus('error') setErrorMessage(acceptData.error || 'Failed to accept invitation') } return } setStatus('accepted') toast.success(`Welcome to ${info.teamName}!`, { description: 'You have successfully joined the team' }) // Redirect to dashboard after a short delay setTimeout(() => { router.push('/dashboard/settings/teams') }, 1500) } catch { setStatus('error') setErrorMessage('Failed to validate invitation') } } validateAndAccept() }, [token, user, authLoading, router]) // Accept invitation (manual fallback) const handleAccept = async () => { setStatus('accepting') try { const response = await fetch(`/api/v1/team-invitations/${token}/accept`, { method: 'POST', credentials: 'include' }) const data = await response.json() if (!response.ok) { if (data.code === 'ALREADY_MEMBER') { setStatus('already_member') } else if (data.code === 'EMAIL_MISMATCH') { setStatus('email_mismatch') setErrorMessage(data.error) } else if (data.code === 'INVITATION_EXPIRED') { setStatus('expired') } else { setStatus('error') setErrorMessage(data.error || 'Failed to accept invitation') } return } setStatus('accepted') toast.success(`Welcome to ${invitationInfo?.teamName}!`, { description: 'You have successfully joined the team' }) // Redirect to dashboard after a short delay setTimeout(() => { router.push('/dashboard/settings/teams') }, 1500) } catch { setStatus('error') setErrorMessage('Failed to accept invitation') } } // Build auth URLs with invitation context const buildAuthUrl = (path: string) => { const params = new URLSearchParams({ callbackUrl: `/accept-invite/${token}`, fromInvite: 'true', }) if (invitationInfo?.email) { params.set('email', invitationInfo.email) } // Pass token for signup to skip email verification if (path === '/signup') { params.set('inviteToken', token) } return `${path}?${params.toString()}` } const loginUrl = buildAuthUrl('/login') const signupUrl = buildAuthUrl('/signup') // Loading state if (authLoading || status === 'loading') { return (

Validating invitation...

) } return (
{/* Header with icon */}

Team Invitation

{invitationInfo && (

You've been invited to join {invitationInfo.teamName}

)}
{/* Requires Authentication */} {status === 'requires_auth' && invitationInfo && (

{invitationInfo.inviterName} has invited you to join {invitationInfo.teamName} as a {invitationInfo.role}.

This invitation was sent to {invitationInfo.email}

Please sign in or create an account to accept this invitation.

)} {/* Valid - Ready to Accept */} {status === 'valid' && invitationInfo && (

{invitationInfo.inviterName} has invited you to join {invitationInfo.teamName} as a {invitationInfo.role}.

)} {/* Accepting */} {status === 'accepting' && (

Accepting invitation...

)} {/* Accepted */} {status === 'accepted' && ( Welcome to the team! You've successfully joined {invitationInfo?.teamName}. Redirecting to your dashboard... )} {/* Already Member */} {status === 'already_member' && ( Already a member You're already a member of this team. )} {/* Email Mismatch */} {status === 'email_mismatch' && ( Email mismatch {errorMessage}
)} {/* Not Found */} {status === 'not_found' && ( Invitation not found This invitation link is invalid or has already been used. Please contact the team owner for a new invitation. )} {/* Expired */} {status === 'expired' && ( Invitation expired This invitation has expired. Please contact the team owner for a new invitation. )} {/* Generic Error */} {status === 'error' && ( Error {errorMessage || 'An error occurred. Please try again.'} )}
) } export default getTemplateOrDefaultClient('app/(auth)/accept-invite/[token]/page.tsx', AcceptInvitePage)